<!--
 * @Description:
 * @Author bianpengfei
 * @create 2021/5/17 15:25
 -->
<template>
  <div class="page">
    <!--上下文菜单（右击鼠标显示）-->
    <transition name="contextmenu" appear>
      <ul
        v-if="contextMenu.visible"
        :style="{ left: contextMenu.left + 'px', top: contextMenu.top + 'px' }"
        class="contextmenu"
      >
        <li
          v-for="(item, index) in _contextMenu"
          :key="index"
          :class="{
            disabled: item.disabled
          }"
          @click.stop="onChooseContextMenu(item)"
        >
          <i :class="item.icon"></i>
          <span>{{ item.label }}</span>
        </li>
      </ul>
    </transition>

    <div class="page__body">
      <!--左侧菜单-->
      <section class="page__body-lt">
        <left-menu @addNode="addNode"></left-menu>
      </section>

      <!--中间内容-->
      <section class="page__body-ct" tabindex="10">
        <div
          ref="efContainerParent"
          v-flow-drag
          v-flow-wheel
          class="efContainer__wrap body-ct-container"
          @contextmenu.prevent.stop="onContextMenu"
          @click="onClickContainer"
        >
          <div id="efContainer" ref="efContainer">
            <template v-for="node in data.nodeList">
              <el-popover
                placement="top-start"
                title="标题"
                width="200"
                trigger="hover"
                content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。"
              >
                <flow-node
                  :id="node.id"
                  :key="node.id"
                  slot="reference"
                  :node-info="node"
                  :active-element.sync="activeElement"
                  @changeNodeSite="changeNodeSite"
                  @clickNode="clickNode"
                  @onItemContextMenu="onContextMenu"
                  @nodeDblclick="onNodeDblclick"
                >
                </flow-node>
              </el-popover>
            </template>
          </div>
        </div>
        <div class="body-ct__footer">
          <div>&nbsp;</div>
          <div class="slider__wrap">
            <span>{{ zoom }}%</span>
            <i class="el-icon-minus" @click="onAddZoomSlider('sub')"></i>
            <el-slider
              v-model="zoom"
              :max="250"
              :min="10"
              size="mini"
              :show-tooltip="false"
              @change="onChangeZoomSlider"
            ></el-slider>
            <i class="el-icon-plus" @click="onAddZoomSlider('add')"></i>
          </div>
        </div>
      </section>

      <section class="page__body-rt"></section>
    </div>

    <!--编辑节点/线 信息-->
    <transition name="scale" appear>
      <el-dialog
        v-if="visible.edit"
        :visible.sync="visible.edit"
        width="400px"
        title="编辑"
        modal-append-to-body
        append-to-body
        :close-on-click-modal="false"
      >
        <node-form
          ref="nodeForm"
          :data.sync="data"
          :visible.sync="visible.edit"
          @updateJsPlumbLine="updateJsPlumbLine"
          @repaintEverything="repaintEverything"
        ></node-form>
      </el-dialog>
    </transition>
  </div>
</template>

<script>
import FlowNode from './components/flow-node'
import LeftMenu from './components/left-menu'
import NodeForm from './components/node-form'
import { jsplumbMixins } from './mixins'
import BUtils, { loadJsSync, deepClone, Decimal } from '@bianpengfei/utils'
import { getDataB } from './mock/data_B'
import directives from './directives'

const JSPLUMB_URL = `${PUBLIC_URL}__inject/js/jsplumb.min.js`

const sub = (arg1, arg2) => +new Decimal(+arg1 || 0).sub(+arg2 || 0).toFixed()

export default {
  name: 'Jsplumb',
  components: {
    FlowNode,
    LeftMenu,
    NodeForm
  },
  mixins: [jsplumbMixins],
  data() {
    return {
      contextMenu: {
        visible: false,
        left: 0,
        top: 0
      },

      visible: {
        edit: false
      },

      zoom: 100,

      pasteNode: null, // 复制的节点

      // jsPlumb 实例
      jsPlumb: null,
      // 控制画布销毁
      easyFlowVisible: true,
      // 控制流程数据显示与隐藏
      flowInfoVisible: false,
      // 是否加载完毕标志位
      loadEasyFlowFinish: false,
      // 数据
      data: {
        name: '',
        nodeList: [],
        lineList: []
      },
      // 激活的元素、可能是节点、可能是连线
      activeElement: {
        // 可选值 node 、line
        type: undefined,
        // 节点ID
        nodeId: undefined,
        // 连线ID
        sourceId: undefined,
        targetId: undefined
      }
    }
  },

  computed: {
    _contextMenu() {
      const { type } = this.activeElement
      switch (type) {
        case 'node':
          return [
            { handle: 'rename', label: '修改', type: 'node', icon: 'el-icon-edit' },
            { handle: 'copy', label: '复制', type: 'node', icon: 'el-icon-copy-document' },
            { handle: 'del', label: '删除节点', type: 'node', icon: 'el-icon-delete' }
          ]
        case 'line':
          return [
            { handle: 'del', label: '删除节点', type: 'line', icon: 'el-icon-delete' },
            { handle: 'rename', label: '修改', type: 'line', icon: 'el-icon-edit' }
          ]
        default:
          return [
            { handle: 'info', label: '应用信息', type: 'app', icon: 'el-icon-view' },
            { handle: 'paste', label: '粘贴', type: 'node', icon: 'el-icon-document-copy', disabled: !this.pasteNode },
            { handle: 'refresh', label: '刷新', type: 'node', icon: 'el-icon-refresh-left' }
          ]
      }
    }
  },

  directives: {
    ...directives
  },

  async mounted() {
    // eslint-disable-next-line no-undef
    await loadJsSync(JSPLUMB_URL)
    if (!window.jsPlumb) {
      this.$message.error('资源请求失败')
      return
    }

    this.jsPlumb = window.jsPlumb.getInstance()
    this.$nextTick(() => {
      // 默认加载流程A的数据、在这里可以根据具体的业务返回符合流程数据格式的数据即可
      this.reloadData(getDataB())
    })

    /* 监听*/
    this.onCloseItemContextMenu = () => {
      this.contextMenu = {
        ...this.contextMenu,
        visible: false
      }
    }

    /* 鼠标移动监听*/
    this.handleMouseMove = (event) => {
      this._mouseEvent = event // 鼠标移动属性
    }
    this.throttleMouseMoveHandler = BUtils.throttle(this.handleMouseMove, 50)

    /* 监听复制*/
    this.handlePaste = (event) => {
      const reactParent = document.querySelector('.page__body-ct')
      if (
        this._mouseEvent?.clientX < reactParent.left ||
        this._mouseEvent?.clientX > reactParent.right ||
        this._mouseEvent?.clientY < reactParent.top ||
        this._mouseEvent?.clientY > reactParent.bottom
      ) {
        return
      }
      if (!this.pasteNode) return
      this.addNode(
        {
          originalEvent: this._mouseEvent
        },
        this.pasteNode
      )
    }

    this.addEventListener()
  },

  activated() {
    this.addEventListener()
  },

  deactivated() {
    this.removeEventListener()
  },

  beforeDestroy () {
    this.removeEventListener()
  },

  methods: {
    // 加载流程图
    reloadData (data) {
      this.easyFlowVisible = false
      this.data = {
        ...this.data,
        nodeList: [],
        lineList: []
      }
      this.$nextTick(() => {
        this.easyFlowVisible = true
        this.data = deepClone(data)
        console.log('this.data', this.data)
        this.$nextTick(() => {
          this.jsPlumb = window.jsPlumb.getInstance()
          this.$nextTick(() => {
            this.jsPlumbInit()
          })
        })
      })
    },

    /* 添加事件监听*/
    addEventListener () {
      this.$el.addEventListener('paste', this.handlePaste)
      this.$el.addEventListener('mousemove', this.throttleMouseMoveHandler)
      this.$el.addEventListener('click', this.onCloseItemContextMenu)
    },

    /* 移除事件监听*/
    removeEventListener () {
      this.$el.removeEventListener('paste', this.handlePaste)
      this.$el.removeEventListener('mousemove', this.throttleMouseMoveHandler)
      this.$el.removeEventListener('click', this.onCloseItemContextMenu)
    },

    jsPlumbInit () {
      this.jsPlumb.ready(async () => {
        // 导入默认配置
        this.jsPlumb.importDefaults(this.jsplumbSetting)
        // 会使整个jsPlumb立即重绘。
        this.jsPlumb.setSuspendDrawing(false, true)
        // 初始化节点
        this.loadEasyFlow()
      })
    },

    // 加载流程图
    loadEasyFlow () {
      return new Promise((resolve, reject) => {
        const { nodeList, lineList } = this.data
        console.log('nodeList', this.data.nodeList)

        // 初始化节点
        if (Array.isArray(nodeList)) {
          nodeList.forEach((node) => {
            console.log('node', node.id)
            // 设置源点，可以拖出线连接其他节点
            this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions)
            // 设置目标点，其他源点拖出的线可以连接该节点
            this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions)
            if (!node.viewOnly) {
              this.jsPlumb.draggable(node.id, {
                // containment: 'parent',
                stop: (el) => {
                  // 拖拽节点结束后的对调
                  console.log('拖拽结束: ', el)
                }
              })
            }
          })
        }
        console.log('lineList', this.data.lineList)
        // 初始化连线
        if (Array.isArray(lineList)) {
          lineList.forEach((line) => {
            const connParam = {
              source: line.from,
              target: line.to,
              label: line.label || '',
              connector: line.connector || '',
              anchors: line.anchors || undefined,
              paintStyle: line.paintStyle || undefined
            }
            this.jsPlumb.connect(connParam, this.jsplumbConnectOptions)
          })
        }

        this.$nextTick(function () {
          this.loadEasyFlowFinish = true
          resolve()
        })
      })
    },

    /* 右击选择*/
    onContextMenu (event, type, data = {}) {
      this.activeElement.type = type
      if (type == 'node') {
        this.activeElement.nodeId = data.id
      } else if (type == 'line') {
        this.activeElement.sourceId = data.sourceId
        this.activeElement.targetId = data.targetId
      }
      this.contextMenu.visible = false
      this.$nextTick(() => {
        this.contextMenu = {
          ...this.contextMenu,
          visible: true,
          left: event.clientX,
          top: event.clientY
        }
      })
    },

    /* 双击*/
    onNodeDblclick (event, type, data = {}) {
      this.visible.edit = true
      this.$nextTick(() => {
        this.$refs.nodeForm.setNode(data)
      })
    },

    /* 点击*/
    onClickContainer () {
      this.contextMenu = {
        visible: false,
        ...this.contextMenu
      }
    },

    /* 选择上下文菜单选项*/
    onChooseContextMenu (value = {}) {
      //   { handle: 'copy', label: '复制', type: 'node', icon: 'el-icon-copy-document' },
      if (value.disabled) return
      const map = new Map().
        set('copy', (info) => {
          const { label, type } = info
          if (type == 'node') {
            this.pasteNode = this.data.nodeList.find((v) => v.id == this.activeElement.nodeId)
          } else if (type == 'line') {
          }
        }).
        set('del', this.deleteElement).
        set('rename', () => {
          this.visible.edit = true
          console.log('测试', value.type)
          if (value.type == 'node') {
            this.$nextTick(() => {
              this.$refs.nodeForm.setNode(this.data.nodeList.find((v) => v.id == this.activeElement.nodeId) || {})
            })
          } else if (value.type == 'line') {
            this.$nextTick(() => {
              this.$refs.nodeForm.setLine(
                this.data.lineList.find(
                  (v) => v.from == this.activeElement.sourceId && v.to == this.activeElement.targetId
                ) || {}
              )
            })
          }
        }).
        set('info', () => {}).
        set('paste', this.handlePaste).
        set('default', () => {})
      map.get(value.handle || 'default')?.(value)
      this.$nextTick(() => {
        this.contextMenu = {
          ...this.contextMenu,
          visible: false
        }
      })
    },

    // 返回唯一标识 随机数 + 时间戳
    getUUID () {
      return Math.random().toString(36).substring(2) + '_' + Number(new Date()).toString()
    },

    // 删除激活的元素
    deleteElement () {
      if (this.activeElement.type === 'node') {
        this.deleteNode(this.activeElement.nodeId)
      } else if (this.activeElement.type === 'line') {
        var conn = this.jsPlumb.getConnections({
          source: this.activeElement.sourceId,
          target: this.activeElement.targetId
        })[0]
        this.jsPlumb.deleteConnection(conn)
      }
    },

    // 删除线
    deleteLine (from, to) {
      this.data.lineList = this.data.lineList.filter(function (line) {
        if (line.from == from && line.to == to) {
          return false
        }
        return true
      })
    },
    // 改变连线
    changeLine (oldFrom, oldTo) {
      this.deleteLine(oldFrom, oldTo)
    },
    // 改变节点的位置
    changeNodeSite (data) {
      for (var i = 0; i < this.data.nodeList.length; i++) {
        let node = this.data.nodeList[i]
        if (node.id === data.nodeId) {
          node.left = data.left
          node.top = data.top
        }
      }
    },

    /**
     * 拖拽结束后添加新的节点
     * @param evt
     * @param nodeMenu 被添加的节点对象
     */
    addNode (evt, nodeMenu) {
      console.log('addNode emit', evt, nodeMenu)
      const clientX = evt.originalEvent.clientX
      const clientY = evt.originalEvent.clientY
      const efContainerParent = this.$refs.efContainerParent
      const containerParentRect = efContainerParent.getBoundingClientRect()
      // 计算是否拖入到容器中
      if (
        clientX < containerParentRect.left ||
        clientX > containerParentRect.right ||
        clientY < containerParentRect.top ||
        clientY > containerParentRect.bottom
      ) {
        this.$message.warning('请把节点拖入到画布中')
        return
      }

      const firstChild = efContainerParent.querySelector('div:first-child')
      const containerRect = firstChild.getBoundingClientRect()
      let scale = 1

      if (firstChild.dataset.scale) {
        scale = Number(firstChild.dataset.scale.replace(/scale\(|\)|\s|/g, '') || 1)
      }

      let left = +new Decimal(+clientX).sub(containerRect.left).sub(86).div(scale).toFixed()
      let top = +new Decimal(+clientY).sub(containerRect.top).sub(16).div(scale).toFixed()

      console.log('ceshi', left, top)
      const nodeId = this.getUUID()
      // 动态生成名字
      let nodeName = nodeMenu.name
      let count =
        this.data.nodeList.reduce((p, v) => {
          if (nodeName.replace(/(（.*）)/g, '') == v.name.replace(/(（.*）)/g, '')) {
            p++
          }
          return p
        }, 0) ?? 0

      if (count > 0) {
        nodeName += `（${String(count)}）`
      }
      console.log(this.data)
      const node = {
        id: nodeId,
        name: nodeName,
        type: nodeMenu.type,
        left: left + 'px',
        top: top + 'px',
        icon: nodeMenu.icon,
        state: 'success'
      }
      /**
       * 这里可以进行业务判断、是否能够添加该节点
       */
      this.data.nodeList.push(node)

      console.log('jsplumbSourceOptions', this.jsplumbSourceOptions)

      this.$nextTick(function () {
        this.jsPlumb.makeSource(nodeId, this.jsplumbSourceOptions)
        this.jsPlumb.makeTarget(nodeId, this.jsplumbTargetOptions)
        this.jsPlumb.draggable(nodeId, {
          // containment: 'parent',
          stop: (el) => {
            // 拖拽节点结束后的对调
            console.log('拖拽结束: ', el)
          }
        })
      })
    },
    /**
     * 删除节点
     * @param nodeId 被删除节点的ID
     */
    deleteNode (nodeId) {
      /**
       * 这里需要进行业务判断，是否可以删除
       */
      this.data.nodeList = this.data.nodeList.filter((node) => {
        if (node.id === nodeId) {
          // 伪删除，将节点隐藏，否则会导致位置错位
          node.show = false
          return false
        }
        return true
      })
      this.$nextTick(function () {
        this.jsPlumb.removeAllEndpoints(nodeId)
      })
      return true
    },

    /* 点击节点*/
    clickNode (nodeId) {
      this.activeElement.type = 'node'
      this.activeElement.nodeId = nodeId
      // this.$refs.nodeForm.nodeInit(this.data, nodeId)
    },

    /* 是否具有该线*/
    hasLine (from, to) {
      return this.data.lineList.some((v) => {
        return v.from === from && v.to === to
      })
    },

    // 是否含有相反的线
    hashOppositeLine (from, to) {
      return this.hasLine(to, from)
    },

    /* 重绘*/
    repaintEverything () {
      this.jsPlumb.repaint()
    },

    // 重新设置连线条件属性
    updateJsPlumbLine (line = {}) {
      const conn = this.jsPlumb.getConnections({
        source: line.from,
        target: line.to
      })[0]
      if (!line.label || line.label === '') {
        conn.removeClass('flowLabel')
        conn.addClass('emptyFlowLabel')
      } else {
        conn.addClass('flowLabel')
      }
      conn.setLabel({
        label: line.label
      })
    },

    /* 缩放区域变化*/
    onChangeZoomSlider (zoom) {
      const el = this.$refs.efContainer
      if (!el.dataset.translate3d) {
        el.dataset.translate3d = 'translate3d(0px, 0px, 0px)'
      }
      const scale = BUtils.div(+zoom, 100)
      el.dataset.scale = `scale(${scale})`
      el.style.transform = `${el.dataset.translate3d} scale(${scale})`
    },

    /* 缩放区域 变大或变小*/
    onAddZoomSlider (type = 'add', unit = 10) {
      let zoom = this.zoom
      if (type == 'add') {
        zoom = BUtils.add(this.zoom, unit)
        if (zoom > 250) {
          return
        }
        this.zoom = zoom
      } else if (type == 'sub') {
        zoom = sub(this.zoom, unit)
        if (zoom < 10) {
          return
        }
        this.zoom = zoom
      }
      this.$nextTick(() => {
        const el = this.$refs.efContainer
        if (!el.dataset.translate3d) {
          el.dataset.translate3d = 'translate3d(0px, 0px, 0px)'
        }
        const scale = BUtils.div(zoom, 100)
        el.dataset.scale = `scale(${scale})`
        el.style.transform = `${el.dataset.translate3d} scale(${scale})`
      })
    },

    // 流程数据信息
    dataInfo () {
      this.flowInfoVisible = true
      this.$nextTick(function () {
        this.$refs.flowInfo.init()
      })
    },

    // 是否是火狐浏览器
    isFirefox () {
      var userAgent = navigator.userAgent
      if (userAgent.indexOf('Firefox') > -1) {
        return true
      }
      return false
    }
  }
}
</script>

<style scoped lang="scss">
@import './index';
</style>

<style lang="scss">
/* 连线中的label 样式*/
.jtk-overlay {
  cursor: pointer;
  color: #4a4a4a;
  &.flowLabel:not(.aLabel) {
    padding: 4px 10px;
    background-color: white;
    color: #565758 !important;
    border: 1px solid #b4bdc5;
    border-radius: 5px;
    display: inline-block;
  }
}
.jtk-hover {
  cursor: pointer;
  &.flowLabel {
    border-color: $--color-danger !important;
  }
}
</style>
